/*
 * Copyright (c) 2017-present, CV4J Contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cv4j.core.tpl;

import com.cv4j.core.datamodel.ByteProcessor;
import com.cv4j.core.datamodel.ColorProcessor;
import com.cv4j.core.datamodel.FloatProcessor;
import com.cv4j.core.datamodel.ImageProcessor;
import com.cv4j.core.datamodel.Point;
import com.cv4j.image.util.Tools;
import com.cv4j.uitl.Log;

import java.util.Arrays;
import java.util.List;

/**
 * 模板匹配
 *
 * @author dev
 * @date 2021/07/22
 */
public class TemplateMatch {
    /**
     * tm sqdiff赋范
     */
    public static final int TM_SQDIFF_NORMED = 2;
    /**
     * tm ccorr赋范
     */
    public static final int TM_CCORR_NORMED = 4;
    /**
     * tm ccoeff赋范
     */
    public static final int TM_CCOEFF_NORMED = 6;

    /**
     * 匹配*
     * match
     *
     * @param target - source image contain template or not
     * @param tpl - template
     * @param method method
     * @return FloatProcessor -
     */
    public FloatProcessor match(ImageProcessor target, ImageProcessor tpl, int method) {
        int width = target.getWidth();
        int height = target.getHeight();
        int offx = tpl.getWidth() / 2 + 1;
        int offy = tpl.getHeight() / 2 + 1;
        int[] tplmask = new int[tpl.getWidth() * tpl.getHeight()];
        Arrays.fill(tplmask, 0);
        int rw = width - offx * 2;
        int rh = height - offy * 2;
        float[] result = new float[rw * rh];
        if (target.getChannels() == 3 && tpl.getChannels() == 3) {
            Log.debug("debug", "do noting");
        } else if (target.getChannels() == 1 && tpl.getChannels() == 1) {
            if (method == TM_CCORR_NORMED) {
                generateNCCResult(target, tpl, result, tplmask);
            }
        } else {
            throw new IllegalStateException("\nERR:Image Type is not same...\n");
        }
        return new FloatProcessor(result, rw, rh);
    }

    /**
     * 匹配
     * match
     *
     * @param target - source image contain template or not
     * @param tpl - template
     * @param locations 位置
     * @param method 方法
     * @param threhold threhold
     */
    public void match(
        ImageProcessor target, ImageProcessor tpl, List<Point> locations, int method, double threhold) {
        int width = target.getWidth();
        int height = target.getHeight();
        int tw = tpl.getWidth();
        int th = tpl.getHeight();
        int offx = tpl.getWidth() / 2 + 1;
        int offy = tpl.getHeight() / 2 + 1;
        int raiduswidth = tpl.getWidth() / 2;
        int raidusheight = tpl.getHeight() / 2;
        int[] tplmask = new int[tpl.getWidth() * tpl.getHeight()];
        Arrays.fill(tplmask, 0);
        if (target.getChannels() == 3 && tpl.getChannels() == 3) {
            Log.debug("debug", "do noting");
        } else if (target.getChannels() == 1 && tpl.getChannels() == 1) {
            byte[] data = ((ByteProcessor) target).getGray();
            byte[] tdata = ((ByteProcessor) tpl).getGray();
            float[] meansdev = Tools.calcMeansAndDev(((ByteProcessor) tpl).toFloat(0));
            double[] tDiff = calculateDiff(tdata, meansdev[0]);
            for (int row = offy; row < height - offy; row += 2) {
                for (int col = offx; col < width - offx; col += 2) {
                    int wrow = 0;
                    Arrays.fill(tplmask, 0);
                    for (int subrow = -raidusheight; subrow <= raidusheight; subrow++) {
                        int wcol = 0;
                        for (int subcol = -raiduswidth; subcol <= raiduswidth; subcol++) {
                            if (wrow >= th || wcol >= tw) {
                                continue;
                            }
                            tplmask[wrow * tw + wcol] = data[(row + subrow) * width + (col + subcol)] & 0xff;
                            wcol++;
                        }
                        wrow++;
                    }
                    // calculate the ncc
                    float[] meansDev = Tools.calcMeansAndDev(tplmask);
                    double[] diff = calculateDiff(tplmask, meansDev[0]);
                    double ncc = calculateNcc(tDiff, diff, meansDev[1], meansdev[1]);
                    if (ncc > threhold) {
                        Point mpoint = new Point();
                        mpoint.x1 = col - raiduswidth;
                        mpoint.y1 = row - raidusheight;
                        locations.add(mpoint);
                    }
                }
            }
        } else {
            Log.debug("debug", "do nothing");
        }
    }

    /**
     * 生成nccresult
     *
     * @param target 目标
     * @param tpl tpl
     * @param result 结果
     * @param tplmask tplmask
     */
    private void generateNCCResult(ImageProcessor target, ImageProcessor tpl, float[] result, int[] tplmask) {
        int width = target.getWidth();
        int height = target.getHeight();
        int tw = tpl.getWidth();
        int th = tpl.getHeight();
        int offX = tpl.getWidth() / 2 + 1;
        int offY = tpl.getHeight() / 2 + 1;
        int raidusWidth = tpl.getWidth() / 2;
        int raidusHeight = tpl.getHeight() / 2;
        byte[] data = ((ByteProcessor) target).getGray();
        byte[] tdata = ((ByteProcessor) tpl).getGray();
        float[] meansdev = Tools.calcMeansAndDev(((ByteProcessor) tpl).toFloat(0));
        double[] tDiff = calculateDiff(tdata, meansdev[0]);

        int rw = width - offX * 2;
        int rh = height - offY * 2;

        for (int row = offY; row < height - offY; row += 2) {
            for (int col = offX; col < width - offX; col += 2) {
                int wrow = 0;
                Arrays.fill(tplmask, 0);
                for (int subrow = -raidusHeight; subrow <= raidusHeight; subrow++) {
                    int wcol = 0;
                    for (int subcol = -raidusWidth; subcol <= raidusWidth; subcol++) {
                        if (wrow >= th || wcol >= tw) {
                            continue;
                        }
                        tplmask[wrow * tw + wcol] = data[(row + subrow) * width + (col + subcol)] & 0xff;
                        wcol++;
                    }
                    wrow++;
                }
                // calculate the ncc
                float[] meansDev = Tools.calcMeansAndDev(tplmask);
                double[] diff = calculateDiff(tplmask, meansDev[0]);
                double ncc = calculateNcc(tDiff, diff, meansDev[1], meansdev[1]);
                result[(row - offY) * rw + (col - offX)] = (float) ncc;
            }
        }
    }

    /**
     * 计算差异
     *
     * @param pixels 像素
     * @param mean 的意思是
     * @return {@link double[]}
     */
    private double[] calculateDiff(byte[] pixels, float mean) {
        double[] diffs = new double[pixels.length];
        int length = diffs.length;
        for (int i = 0; i < length; i++) {
            diffs[i] = (int) (pixels[i] & 0xff) - mean;
        }
        return diffs;
    }

    /**
     * 计算差异
     *
     * @param pixels 像素
     * @param mean 的意思是
     * @return {@link double[]}
     */
    private double[] calculateDiff(int[] pixels, float mean) {
        double[] diffs = new double[pixels.length];
        int length = diffs.length;
        for (int i = 0; i < length; i++) {
            diffs[i] = pixels[i] - mean;
        }
        return diffs;
    }

    /**
     * 计算ncc
     *
     * @param tDiff t diff
     * @param diff diff
     * @param dev1 dev1
     * @param dev2 dev2
     * @return double
     */
    private double calculateNcc(double[] tDiff, double[] diff, double dev1, double dev2) {
        double sum = 0.0d;
        double count = diff.length;
        for (int i = 0; i < diff.length; i++) {
            sum += ((tDiff[i] * diff[i]) / (dev1 * dev2));
        }
        return (sum / count);
    }
}
